目前在我的玩具 JS 引擎 naive 中还没有实现 Generator,所以这篇除了可以当做是预先调研之外,也可以作为稍微深入到 Generator 实现细节的介绍
由于 Generator Function 和普通函数长得太像了,所以先回顾一下普通函数实现细节
我们知道在 C 语言中函数最终体现为代码段中的一段内容,该段内容就是函数体内容对应的机器码,而函数名则为该段代码的首地址,在编译期间会被替换处理
但是在学习 JS 的时候,并不是每个地方都能以 C 语言来参考,这里把 C 换成任何其他语言也说得通;可以在一些问题发生的时候,使用已经掌握的知识来参考联想,但是最终还是要回归到该语言自身的技术规格文档中
在 JS 中,函数作为一个对象,该对象包含了以下几个内容:
当我们调用函数的时候,引擎就会做以下的事情:
CallInfo
而 CallInfo
,包含下面的内容:
this
对象,用于在执行 THIS
指令的时候取得对应的对象另外 JS 引擎在运行时会用到两个栈结构,一个作为调用栈,一个作为操作数栈我们上面介绍的是调用栈,而操作数栈用于存放指令执行时所用到的操作数,包括局部变量和临时变量
我们来看一个典型的生成器函数的例子:
function* idMaker() {
var index = 0;
while(true)
yield index++;
}
想必大家第一次接触到这个语法的时候一头雾水,因为一直以来 while(true){ /* no break or return */ }
这样的形式,直接告诉我们该循环为一个死循环恰好上面的例子中,while
语句中也没有 break
和 return
,如果是死循环,那么这段代码肯定就失去意义了,如果不是死循环,又打破了我们之前的认知
其实 yield
语句,不过是一个语法糖(Syntactic sugar)所谓语法糖就是一些方便程序员书写代码的语法,它们总能找到不使用该语法的对应写法如果我们参考了技术规格文档,那么发现其实 yield
隐含了 return
的语义,知道这个就放心了,原来我们之前的认知是准确无误的
在了解了 yield
隐含了 return
的语义后,死循环的问题可以先不用考虑了,但是产生了一个新的问题:在普通函数中,一旦 return
出去了,等于函数主动放弃了执行权,那么再没有办法恢复之前的执行状态但是我们的生成器函数,是可以不断执行的,换句话说,在 yield
隐含的 return
语义生效后依然可以保持执行状态,以便下一次执行时从上一次暂停点继续执行
因为 JS 引擎是一个单线程的引擎,所以同一时间内,只有一段 JS 代码(函数)会被执行,换句话说,这段时间内执行的代码就占据了程序(引擎)的控制权,而当当前函数(callee)执行完毕后,引擎转而继续执行 caller 中的内容,就称函数交出了对引擎的控制权我们可以显式地交出控制权,通过显式地 return
语句;或者隐式地交出控制权,通过编译器在每个函数末尾自动插入的 return
语句之所以普通函数 return
之后无法再之前恢复状态是因为,保存之前调用信息的 CallInfo
对象已经从调用栈中被移除了,也就无法知道之前的 PC
等信息了
不知道大家看到这里会不会有灵光一闪的感觉:如果 CallInfo
对象在调用结束后,能以一种方式保存它,那么后续再次调用它时,不就能继续执行了吗?没错,生成器函数本质上就是这个道理